import json

import google.protobuf.json_format as json_format

from cc.appcoin2.support.order.py_parser import PyOrderParser
import coin.exchange.base.kr_rest.private_client_base as privcb
import coin.exchange.binance_futures_swap.kr_rest.native_private_client as nprivc
from coin.exchange.binance_futures.enum_types import (BinanceFuturesOrderSide,
                                                      BinanceFuturesOrderType,
                                                      BinanceFuturesTimeInForce)
from coin.exchange.binance_futures.kr_rest.currency import BinanceCurrency
from coin.exchange.binance_futures.kr_rest.futures_product import BinanceFuturesProduct
from coin.exchange.binance_futures_swap.kr_rest.product_info import binance_futures_swap_product_info
import coin.proto.coin_order_enums_pb2 as coin_order
from coin.proto.coin_query_pb2 import (AccountBalance,
                                       AccountPosition,
                                       ProductPosition,
                                       ProductOrderElement,
                                       ProductOrderBundle,
                                       CurrencyBalance,
                                       ProductFillElement,
                                       ProductFillBundle)
from coin2.exchange.order.system_pb2 import OrderSystemConfig
from coin2.exchange.symbology_pb2 import ProductConfig


class BinanceSwapFuturesPrivateParser(object):
  @staticmethod
  def parse_balance(balance_msg: dict):
    assert isinstance(balance_msg, dict), balance_msg
    assert 'assets' in balance_msg, balance_msg
    balance_list = []
    for update_msg in balance_msg['assets']:
      currency = BinanceCurrency.FromStrNativeCurrencyNothrow(update_msg['asset'])
      if currency is None:
        continue

      hold = float(update_msg['initialMargin'])
      total = float(update_msg['marginBalance'])
      available = total - hold + float(update_msg['unrealizedProfit'])
      currency_balance = CurrencyBalance(
          currency=currency.currency,
          available=available,
          hold=hold,
          total=total,
      )
      balance_list.append(currency_balance)
    return AccountBalance(exchange='Binance', each_balance=balance_list)

  @staticmethod
  def parse_unrealized_pnl(balance_msg: dict):
    assert isinstance(balance_msg, dict), balance_msg
    unrealized_pnl = {}
    for update_msg in balance_msg['assets']:
      currency = BinanceCurrency.FromStrNativeCurrencyNothrow(update_msg['asset'])
      if currency is None:
        continue
      unrealized_pnl[currency.currency] = float(update_msg['unrealizedProfit'])
    return unrealized_pnl

  @staticmethod
  def parse_account_position(update_msg):
    assert isinstance(update_msg, list), update_msg
    position_list = []
    for pos_info in update_msg:
      native_symbol = pos_info['symbol']
      product = BinanceFuturesProduct.FromStrNativeProduct(native_symbol)
      position = ProductPosition(symbol=product.symbol)
      pos_amt = float(pos_info['positionAmt'])

      if pos_amt >= 0:
        position.long_position = pos_amt
      elif pos_amt <= 0:
        position.short_position = -pos_amt
      position.net_position = pos_amt
      position.avg_entry_price = float(pos_info['entryPrice'])
      position_list.append(position)

    return AccountPosition(exchange='Binance', each_position=position_list)

  @staticmethod
  def parse_fills(update_msg: list, product) -> ProductFillBundle:
    """Only first 500 filled orders are retrieved."""
    currency_fill_list = []
    for update_elem in update_msg:
      currency_fill = ProductFillElement(symbol=product.symbol,
                                         price=float(update_elem['price']),
                                         qty=float(update_elem['qty']),
                                         order_id=str(update_elem['orderId']),
                                         fill_timestamp=int(update_elem['time']) * 10**6,
                                         fee=float(update_elem['commission']),
                                         fee_currency=str(update_elem['commissionAsset']))
      currency_fill_list.append(currency_fill)

    return ProductFillBundle(exchange='Binance',
                             symbol=product.symbol,
                             each_fill=currency_fill_list)

  @staticmethod
  def parse_orders(update_msg: list, product):
    open_order_list = []
    for update_elem in update_msg:
      assert update_elem['symbol'] == product.native_symbol, update_elem
      if update_elem['side'] == 'SELL':
        side = coin_order.SELL_ORDER
      elif update_elem['side'] == 'BUY':
        side = coin_order.BUY_ORDER
      else:
        raise ValueError(update_elem['side'])

      order = ProductOrderElement(symbol=product.symbol,
                                  side=side,
                                  price=float(update_elem['price']),
                                  qty=float(update_elem['origQty']),
                                  order_id=str(update_elem['orderId']))
      open_order_list.append(order)
    return ProductOrderBundle(
        exchange='Binance',
        symbol=product.symbol,
        each_order=open_order_list,
    )


class BinanceSwapFuturesPrivateClient(privcb.PrivateClientBase):
  ProductType = BinanceFuturesProduct

  def __init__(self, key_file):
    privcb.PrivateClientBase.__init__(self)
    self.nprivc = nprivc.BinanceSwapNativePrivateClient(key_file=key_file)
    pi_bundle = binance_futures_swap_product_info._product_infos
    assert pi_bundle.HasField('mea')
    oss_config = OrderSystemConfig(
        mea=pi_bundle.mea,
        products=ProductConfig(norms=[pi.symbol for pi in pi_bundle.product_infos]))
    self._parser = PyOrderParser.from_mea_str(pi_bundle.mea)
    self._parser.add_product(oss_config.SerializeToString())

  def query_account_balance_impl(self):
    update = self.nprivc.query_account()
    msg = self._parser.parse_account_balance(json.dumps(update.msg))
    account_balance = AccountBalance()
    json_format.Parse(msg, account_balance)
    update.msg = account_balance
    return update

  def query_account_position_impl(self):
    update = self.nprivc.query_position_risk()
    msg = self._parser.parse_account_position(json.dumps(update.msg))
    account_position = AccountPosition()
    json_format.Parse(msg, account_position)
    update.msg = account_position
    return update

  def query_fills_impl(self, product):
    update = self.nprivc.query_trade_list(symbol=product.native_symbol)
    update.msg = BinanceSwapFuturesPrivateParser.parse_fills(update.msg, product)
    return update

  def query_list_orders_impl(self, product):
    symbol = product.native_symbol
    update = self.nprivc.query_open_orders(symbol=symbol)
    update.msg = BinanceSwapFuturesPrivateParser.parse_orders(update.msg, product)
    return update

  def query_submit_on_type(self, time_in_force, product, side, price, qty):
    assert price > 0, (product, side, price, qty)
    assert side in ('buy', 'sell'), side
    symbol = product.native_symbol
    price_str = "%.8f" % price
    return self.nprivc.place_order(time_in_force=BinanceFuturesTimeInForce(time_in_force),
                                   symbol=symbol,
                                   order_side=BinanceFuturesOrderSide(side.upper()),
                                   order_type=BinanceFuturesOrderType.LIMIT,
                                   price=price_str,
                                   quantity=qty)

  def query_submit_ioc(self, product, side, price, qty):
    self._check_product_type(product)
    update = self.query_submit_ioc_impl(product, side, price, qty)
    return update

  def query_submit_ioc_impl(self, product, side, price, qty):
    return self.query_submit_on_type('IOC', product, side, price, qty)

  def query_submit_impl(self, product, side, price, qty):
    return self.query_submit_on_type('GTC', product, side, price, qty)

  def query_cancel(self, product, order_id):
    return self.nprivc.cancel_order(symbol=product.native_symbol, order_id=order_id)

  def query_cancel_product_impl(self, product):
    updates = []
    update_list = self.query_list_orders(product)
    for each_order in update_list.msg.each_order:
      updates.append(self.query_cancel(product, each_order.order_id))
    return updates
